昨天我們成功建立了測試環境並寫下第一個測試,今天要深入了解測試的核心 —「斷言(Assertions)」。
想像一下,你正在開發一個使用者註冊功能。產品經理說:「我們需要驗證使用者輸入的 email 格式、密碼強度、年齡範圍...」你心想:「這麼多驗證規則,怎麼確保每一個都正確運作?」
答案就是斷言!斷言是測試的核心,它告訴我們「期望」和「實際」結果是否相符。今天我們要學會使用各種斷言方法,讓測試更精準、更具表達力。
今天結束後,你將學會:
第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions) ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
斷言就像品質檢驗員,負責檢查產品是否符合規格。在測試中,斷言:
昨天我們已經使用了最基本的 assert
語句:
建立 tests/day02/test_basic_assertions.py
:
def test_uses_assert_for_equality():
result = 2 + 3
assert result == 5
def test_equality_vs_identity():
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
# == 檢查值相等
assert list1 == list2
# is 檢查是否為同一個物件(身份相等)
assert list1 is not list2
assert list1 is list3
更新 tests/day02/test_basic_assertions.py
:
def test_equality_assertions():
# 相等性檢查
assert 5 == 5
assert 'hello' == 'hello'
# 不相等檢查
assert 5 != 10
assert 'hello' != 'world'
def test_membership_assertions():
# in 成員檢查
assert 'h' in 'hello'
assert 3 in [1, 2, 3, 4, 5]
# not in 檢查
assert 'x' not in 'hello'
assert 10 not in [1, 2, 3, 4, 5]
def test_none_assertions():
value = None
not_none = 'exists'
assert value is None
assert not_none is not None
def test_boolean_assertions():
assert True
assert not False
# 真值檢查
assert bool(1)
assert bool('hello')
assert not bool(0)
assert not bool('')
def test_type_assertions():
assert isinstance(42, int)
assert isinstance('hello', str)
assert isinstance(3.14, float)
assert isinstance([], list)
assert isinstance({}, dict)
數值比較在驗證功能中經常用到:
建立 tests/day02/test_number_assertions.py
:
def test_greater_than():
assert 10 > 5
def test_greater_than_or_equal():
assert 10 >= 10
assert 15 >= 10
def test_less_than():
assert 5 < 10
def test_less_than_or_equal():
assert 5 <= 5
assert 3 <= 5
def test_range_check():
value = 5
assert 1 <= value <= 10
def test_approximate_equality():
import math
# 使用 pytest.approx 處理浮點數
import pytest
assert 0.1 + 0.2 == pytest.approx(0.3)
assert math.pi == pytest.approx(3.14159, rel=1e-5)
字串驗證在表單處理中非常重要:
建立 tests/day02/test_string_assertions.py
:
def test_string_contains():
assert 'World' in 'Hello World'
assert '@' in 'user@example.com'
def test_string_regex():
import re
assert re.match(r'^hello\d+$', 'hello123')
assert re.match(r'\w+@\w+\.\w+', 'test@email.com')
def test_string_length():
assert len('hello') == 5
assert len('') == 0
def test_string_starts_ends():
text = 'Hello World'
assert text.startswith('Hello')
assert text.endswith('World')
def test_string_case():
text = 'Hello World'
assert text.lower() == 'hello world'
assert text.upper() == 'HELLO WORLD'
讓我們實作一個驗證器模組來練習各種斷言:
建立 tests/day02/test_validator.py
:
import pytest
from src.validator import (
is_valid_email,
is_strong_password,
is_valid_age
)
class TestEmailValidation:
def test_accepts_valid_emails(self):
assert is_valid_email('user@example.com')
assert is_valid_email('test.user@company.co.uk')
def test_rejects_invalid_emails(self):
assert not is_valid_email('invalid')
assert not is_valid_email('@example.com')
assert not is_valid_email('user@')
class TestPasswordValidation:
def test_accepts_strong_passwords(self):
assert is_strong_password('MyP@ss123')
assert is_strong_password('Str0ng!Pass')
def test_rejects_weak_passwords(self):
assert not is_strong_password('weak')
assert not is_strong_password('onlylowercase')
assert not is_strong_password('ONLYUPPERCASE')
assert not is_strong_password('NoNumbers!')
class TestAgeValidation:
def test_accepts_valid_age_range(self):
assert is_valid_age(25)
assert is_valid_age(18)
assert is_valid_age(65)
def test_rejects_invalid_ages(self):
assert not is_valid_age(17)
assert not is_valid_age(121)
assert not is_valid_age(-5)
建立 src/validator.py
:
import re
def is_valid_email(email: str) -> bool:
email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
return bool(re.match(email_pattern, email))
def is_strong_password(password: str) -> bool:
if len(password) < 8:
return False
has_upper = bool(re.search(r'[A-Z]', password))
has_lower = bool(re.search(r'[a-z]', password))
has_digit = bool(re.search(r'\d', password))
has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password))
return has_upper and has_lower and has_digit and has_special
def is_valid_age(age: int) -> bool:
return 18 <= age <= 120
執行所有 Day 02 的測試:
pytest tests/day02/ -v
你應該會看到所有測試通過,並且能清楚了解每個斷言的作用。
assert x == y
: 相等比較assert x != y
: 不相等比較assert x in y
: 成員檢查assert x > y
, assert x < y
: 數值比較assert isinstance(x, type)
: 類型檢查今天我們深入學習了斷言的使用,從基本的相等比較到複雜的字串匹配、數值比較。透過實作驗證器函數,我們練習了選擇合適的斷言方法、編寫清晰的測試、分步驟驗證複雜邏輯。
斷言是測試的基石,掌握好斷言的使用,你的測試就會更加準確和有說服力。
明天我們將進入 TDD 的核心 —「紅綠重構循環」! 💪
本文是「Python pytest TDD 實戰:從零開始的測試驅動開發」系列的第二篇文章。我們正在學習測試驅動開發的基礎,從環境設置到掌握核心概念,一步步建立扎實的測試能力。